feat(studio): runtime-first dynamic keyframe system [8/10]#1190
Conversation
6e391b2 to
eca150f
Compare
0dbfe93 to
925397e
Compare
eca150f to
dcbe8da
Compare
925397e to
4711c17
Compare
dcbe8da to
3a8e22f
Compare
4711c17 to
32dd0bb
Compare
3a8e22f to
0eb2b93
Compare
32dd0bb to
7f991c4
Compare
0eb2b93 to
626983d
Compare
jrusso1020
left a comment
There was a problem hiding this comment.
The most architecturally consequential PR in the back half of the stack. +773/-38. Reshapes keyframe discovery from "AST-only" to "runtime-first": Studio reads tween.vars.keyframes from the live iframe rather than relying solely on the AST parse. Plus materializeKeyframesInScript / unrollDynamicAnimations to convert dynamic loops into individual tl.to() calls on first edit.
The key risk: AST↔runtime divergence
The "runtime-first" model means there are now two sources of truth for what a tween is:
- AST parse of the source script.
- Live
tween.varsfrom the running iframe.
Some keyframes (the "dynamic" ones — generated by a JS loop like for (let i = 0; i < 10; i++) tl.to(...)) only exist in the runtime view, not the AST. The hasUnresolvedKeyframes / hasUnresolvedSelector flags + materializeKeyframesInScript are the bridge.
Things to verify:
1. Materialization is idempotent
Calling materializeKeyframesInScript on a script that's already been materialized should be a no-op. Otherwise repeated edits compound the unroll and the script grows linearly with edit count.
2. Runtime scan interval
PR body says "interval-based runtime scan". What interval? If it's too tight (< 200ms), it's a perf cost on every Studio session. If too loose (> 2s), the panel feels stale during animation creation. Worth a comment in the code explaining the chosen value.
3. Cold-load race + retry effect
"Retry effect for cold-load animation fetch race" — this hints at a known timing issue. The retry strategy matters:
- Bounded retries: if the runtime never produces tween data, the retry should eventually give up (not loop infinitely).
- Exponential backoff or fixed interval: either is fine, but document.
- User-visible feedback: if retry is exhausted, the user should see "couldn't load keyframes" rather than an empty panel.
4. unrollDynamicAnimations and edit semantics
When a dynamic loop like for (let i = 0; i < 10; i++) tl.to("#item-" + i, ...) is unrolled into 10 static tl.to() calls on first edit:
- The unroll is destructive — you lose the original loop expression. Confirm this is documented as an intentional, irreversible transformation (user can undo at the AST level, but they can't get the original loop back without writing it again).
- Selector resolution: each unrolled call gets a concrete selector. If the loop used
"#item-" + i, the unrolled output should be"#item-0","#item-1", etc. Verify against tests. - Comment / docstring: the unroll comment ideally says "originally generated by loop at line N" so the human reader understands what happened.
5. easeEach placement fix
The fix is the right call — easeEach belongs inside the keyframes: {} object, not at the tween-vars level. This is consistent with GSAP's documented placement. The fix in this PR likely corrects a regression from earlier in the stack (looks like #1167's tween-level easeEach propagation logic had it at the wrong level for emit but right level for parse). Worth a quick parse↔serialize round-trip test that asserts easeEach ends up in the same JSON shape on both sides.
6. Dual-write cache keys
"Dual-write keys" in the keyframe cache — what's the second key for? Migration from old key shape? Coexistence with the legacy code path? Worth a comment explaining the dual-write window (and when one of them can be removed).
This is a meaty PR — I'd push for a screenshot or video showing the live-runtime keyframe panel responding to a scrub before merging.
Review by Jerrai (hyperframes specialist)
626983d to
7149806
Compare
7f991c4 to
524dc79
Compare
7149806 to
9ee5a4d
Compare
6186562 to
54d1996
Compare
9ee5a4d to
f7407bd
Compare
Revert totalTime nudge that caused black first frames in from() tweens. Keep stale CSS offset cleanup. Regenerate baselines for offset cleanup.
…od baselines Baselines regenerated inside Dockerfile.test on the devbox to match the current runtime init.ts changes. Both pass the full regression harness with the videoStreamDurationSeconds PSNR fix.
…ation U1: stripGsapTranslateFromTransform now rotates the offset vector by the element's CSS rotation angle before subtracting from m41/m42. Fixes elements drifting from cursor during drag when rotated. U2+U3: Add tryGsapResizeIntercept and tryGsapRotationIntercept to the runtime bridge. Resize and rotation handle changes now create keyframes via the same async pipeline as position drag. CSS path guards prevent double-persistence for GSAP-animated elements.
CSS compose order is translate → rotate → transform. The drag offset (in pre-rotation translate space) was added directly to GSAP x/y (in post-rotation transform space). Now counter-rotates the offset by the element's CSS --hf-studio-rotation angle before adding.
Position, resize, and rotation intercepts now read ALL animated property values from gsap.getProperty() at commit time and include them in the keyframe. Prevents other properties from jumping to interpolated values between surrounding keyframes when only one property (e.g., width) was explicitly changed.
Revert totalTime nudge that caused black first frames in from() tweens. Keep stale CSS offset cleanup. Regenerate baselines for offset cleanup.
Revert totalTime nudge that caused black first frames in from() tweens. Keep stale CSS offset cleanup. Regenerate baselines for offset cleanup.
…lization Read GSAP keyframe data from the live runtime instead of only the AST parser. Dynamic keyframes (loops, variables, computed selectors) now show diamonds on timeline clips and animation cards in the design panel. On first edit, dynamic code is automatically materialized: - Unresolved keyframes (keyframes: kf) replaced with static object - Unresolved selectors (tl.to(sel, ...)) entire loop unrolled into individual static tl.to() calls per element Key changes: - Parser: hasUnresolvedKeyframes/hasUnresolvedSelector flags - Runtime bridge: scanAllRuntimeKeyframes reads tween.vars from iframe - Tween cache: interval-based runtime scan for dynamic animations - materializeKeyframesInScript + unrollDynamicAnimations parser functions - Keyframe cache dual-writes both sourceFile#id and index.html#id keys - commitMutation updates cache from mutation response - easeEach placement fix (inside keyframes object, not tween vars)
1a4e04b to
ba5a06a
Compare

Summary
tween.vars.keyframes) instead of AST onlyhasUnresolvedKeyframes/hasUnresolvedSelectorflags on GsapAnimationscanAllRuntimeKeyframesdiscovers all keyframed tweens in the iframematerializeKeyframesInScript+unrollDynamicAnimations— on first edit, dynamic loops are replaced with individual static tl.to() callseaseEachplacement fix (inside keyframes object, not tween vars level)+773 LOC